package owl2vowl.export;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.semanticweb.owlapi.model.IRI;

import owl2vowl.constants.VowlAttribute;
import owl2vowl.model.annotation.Annotation;
import owl2vowl.model.data.VowlData;
import owl2vowl.model.entities.AbstractEntity;
import owl2vowl.model.entities.HasReference;
import owl2vowl.model.entities.nodes.classes.VowlClass;
import owl2vowl.model.entities.nodes.classes.VowlThing;
import owl2vowl.model.entities.nodes.datatypes.AbstractDatatype;
import owl2vowl.model.entities.nodes.datatypes.DatatypeReference;
import owl2vowl.model.entities.nodes.datatypes.LiteralReference;
import owl2vowl.model.entities.nodes.datatypes.VowlDatatype;
import owl2vowl.model.entities.nodes.datatypes.VowlLiteral;
import owl2vowl.model.entities.properties.AbstractProperty;
import owl2vowl.model.entities.properties.TypeOfProperty;
import owl2vowl.model.entities.properties.VowlDatatypeProperty;
import owl2vowl.model.entities.properties.VowlObjectProperty;
import owl2vowl.model.individuals.VowlIndividual;

public class JsonGeneratorVisitorImpl implements JsonGeneratorVisitor {
	private final VowlData vowlData;
	private final Map<String, Object> root;
	private List<Object> _class;
	private List<Object> classAttribute;
	private List<Object> datatype;
	private List<Object> datatypeAttribute;
	private List<Object> propertyList;
	private List<Object> propertyAttributeList;
	private Logger logger = LogManager.getLogger(JsonGeneratorVisitorImpl.class);

	public JsonGeneratorVisitorImpl(VowlData vowlData, Map<String, Object> root) {
		this.vowlData = vowlData;
		this.root = root;
		this.populateJsonRoot();
	}

	public static Map<String, String> getLabelsFromAnnotations(Collection<Annotation> annotations) {
		HashMap languageToValue = new HashMap();
		Iterator arg1 = annotations.iterator();

		while (arg1.hasNext()) {
			Annotation annotation = (Annotation) arg1.next();
			languageToValue.put(annotation.getLanguage(), annotation.getValue());
		}

		return languageToValue;
	}

	private void populateJsonRoot() {
		this._class = new ArrayList();
		this.classAttribute = new ArrayList();
		this.datatype = new ArrayList();
		this.datatypeAttribute = new ArrayList();
		this.propertyList = new ArrayList();
		this.propertyAttributeList = new ArrayList();
		this.root.put("class", this._class);
		this.root.put("classAttribute", this.classAttribute);
		this.root.put("datatype", this.datatype);
		this.root.put("datatypeAttribute", this.datatypeAttribute);
		this.root.put("property", this.propertyList);
		this.root.put("propertyAttribute", this.propertyAttributeList);
	}

	private void addCommonFields(AbstractEntity entity, Map<String, Object> object, Map<String, Object> attributes) {
		object.put("id", this.vowlData.getIdForEntity(entity));
		object.put("type", entity.getType());
		attributes.put("id", this.vowlData.getIdForEntity(entity));
		this.addCommonNonAnonymAttributes(entity, attributes);
	}

	private void addCommonNonAnonymAttributes(AbstractEntity entity, Map<String, Object> attributes) {
		if (!entity.getAttributes().contains(VowlAttribute.ANONYMOUS)) {
			attributes.put("label", getLabelsFromAnnotations(entity.getAnnotations().getLabels()));
			attributes.put("iri", entity.getIri().toString());
			if (entity.getBaseIri() != null) {
				attributes.put("baseIri", entity.getBaseIri().toString());
			}
		}

	}

	public void visit(VowlThing vowlThing) {
		HashMap object = new HashMap();
		HashMap attributes = new HashMap();
		this.addCommonFields(vowlThing, object, attributes);
		attributes.put("iri", VowlThing.GENERIC_THING_IRI.toString());
		this._class.add(object);
		this.classAttribute.add(attributes);
	}

	public void visit(VowlClass vowlClass) {
		HashMap object = new HashMap();
		HashMap attributes = new HashMap();
		this.addCommonFields(vowlClass, object, attributes);
		attributes.put("description", getLabelsFromAnnotations(vowlClass.getAnnotations().getDescription()));
		attributes.put("comment", getLabelsFromAnnotations(vowlClass.getAnnotations().getComments()));
		attributes.put("superClasses", this.getListWithIds(vowlClass.getSuperEntities()));
		attributes.put("subClasses", this.getListWithIds(vowlClass.getSubEntities()));
		attributes.put("annotations", vowlClass.getAnnotations().getIdentifierToAnnotation());
		attributes.put("union", this.getListWithIds(vowlClass.getElementsOfUnion()));
		attributes.put("disjointUnion", this.getListWithIds(vowlClass.getDisjointUnion()));
		attributes.put("intersection", this.getListWithIds(vowlClass.getElementOfIntersection()));
		attributes.put("attributes", vowlClass.getAttributes());
		attributes.put("equivalent", this.getListWithIds(vowlClass.getSortedEquivalents()));
		attributes.put("complement", this.getListWithIds(vowlClass.getComplements()));
		attributes.put("instances", Integer.valueOf(vowlClass.getInstances().size()));
		attributes.put("individuals", this.createIndividualsJson(vowlClass.getIndividuals()));
		this._class.add(object);
		this.classAttribute.add(attributes);
	}

	private Object createIndividualsJson(Set<IRI> individuals) {
		ArrayList individualList = new ArrayList();
		Iterator arg2 = individuals.iterator();

		while (arg2.hasNext()) {
			IRI individualIri = (IRI) arg2.next();
			VowlIndividual individual = (VowlIndividual) this.vowlData.getIndividualMap().get(individualIri);
			HashMap fields = new HashMap();
			fields.put("iri", individual.getIri().toString());
			fields.put("baseIri", individual.getBaseIri().toString());
			fields.put("labels", getLabelsFromAnnotations(individual.getAnnotations().getLabels()));
			fields.put("description", getLabelsFromAnnotations(individual.getAnnotations().getDescription()));
			fields.put("comment", getLabelsFromAnnotations(individual.getAnnotations().getComments()));
			fields.put("annotations", individual.getAnnotations().getIdentifierToAnnotation());
			individualList.add(fields);
		}

		return individualList;
	}

	public void visit(VowlLiteral vowlLiteral) {
		if (vowlLiteral instanceof LiteralReference) {
			AbstractDatatype reference = this.vowlData
					.getDatatypeForIri(((LiteralReference) vowlLiteral).getReferencedIri());
			HashMap object = new HashMap();
			HashMap attributes = new HashMap();
			object.put("id", this.vowlData.getIdForEntity(vowlLiteral));
			object.put("type", reference.getType());
			attributes.put("id", this.vowlData.getIdForEntity(vowlLiteral));
			attributes.put("label", getLabelsFromAnnotations(reference.getAnnotations().getLabels()));
			attributes.put("iri", vowlLiteral.getGenericIri());
			this._class.add(object);
			this.classAttribute.add(attributes);
		}
	}

	public void visit(VowlDatatype vowlDatatype) {
		if (vowlDatatype instanceof DatatypeReference) {
			AbstractDatatype reference = this.vowlData
					.getDatatypeForIri(((DatatypeReference) vowlDatatype).getReferencedIri());
			HashMap object = new HashMap();
			HashMap attributes = new HashMap();
			object.put("id", this.vowlData.getIdForEntity(vowlDatatype));
			object.put("type", reference.getType());
			attributes.put("id", this.vowlData.getIdForEntity(vowlDatatype));
			attributes.put("label", getLabelsFromAnnotations(reference.getAnnotations().getLabels()));
			attributes.put("iri", reference.getIri().toString());
			attributes.put("baseIri", reference.getBaseIri().toString());
			this._class.add(object);
			this.classAttribute.add(attributes);
		}
	}

	public void visit(VowlObjectProperty vowlObjectProperty) {
		this.addProperty(vowlObjectProperty);
	}

	public void visit(VowlDatatypeProperty vowlDatatypeProperty) {
		this.addProperty(vowlDatatypeProperty);
	}

	private void addProperty(AbstractProperty property) {
		if (property instanceof HasReference) {
			this.addReferenceProperty(property);
		} else if (!property.getDomains().isEmpty() && !property.getRanges().isEmpty()) {
			HashMap object = new HashMap();
			HashMap attributes = new HashMap();
			this.addCommonFields(property, object, attributes);
			attributes.put("domain", this.vowlData.getIdForIri(property.getJsonDomain()));
			attributes.put("range", this.vowlData.getIdForIri(property.getJsonRange()));
			attributes.put("description", getLabelsFromAnnotations(property.getAnnotations().getDescription()));
			attributes.put("comment", getLabelsFromAnnotations(property.getAnnotations().getComments()));
			attributes.put("attributes", property.getAttributes());
			attributes.put("annotations", property.getAnnotations().getIdentifierToAnnotation());
			attributes.put("inverse", this.getIdForIri(property.getInverse()));
			attributes.put("superproperty", this.getListWithIds(property.getSuperEntities()));
			attributes.put("subproperty", this.getListWithIds(property.getSubEntities()));
			attributes.put("equivalent", this.getListWithIds(property.getSortedEquivalents()));
			attributes.put("minCardinality", this.getCardinality(property.getMinCardinality()));
			attributes.put("maxCardinality", this.getCardinality(property.getMaxCardinality()));
			attributes.put("cardinality", this.getCardinality(property.getExactCardinality()));
			this.propertyList.add(object);
			this.propertyAttributeList.add(attributes);
		} else {
			this.logger.info("Domain or range is empty in property: " + property);
		}
	}

	private void addReferenceProperty(AbstractProperty property) {
		if (!property.getDomains().isEmpty() && !property.getRanges().isEmpty()) {
			HashMap object = new HashMap();
			HashMap attributes = new HashMap();
			AbstractProperty reference = this.vowlData.getPropertyForIri(((HasReference) property).getReferenceIri());
			object.put("id", this.vowlData.getIdForEntity(property));
			object.put("type", property.getType());
			attributes.put("id", this.vowlData.getIdForEntity(property));
			this.addCommonNonAnonymAttributes(reference, attributes);
			attributes.put("domain", this.vowlData.getIdForIri(property.getJsonDomain()));
			attributes.put("range", this.vowlData.getIdForIri(property.getJsonRange()));
			attributes.put("description", getLabelsFromAnnotations(reference.getAnnotations().getDescription()));
			attributes.put("comment", getLabelsFromAnnotations(reference.getAnnotations().getComments()));
			attributes.put("attributes",
					Stream.concat(property.getAttributes().stream(), reference.getAttributes().stream())
							.collect(Collectors.toSet()));
			attributes.put("annotations", reference.getAnnotations().getIdentifierToAnnotation());
			attributes.put("inverse", this.getIdForIri(reference.getInverse()));
			attributes.put("superproperty", this.getListWithIds(reference.getSuperEntities()));
			attributes.put("subproperty", this.getListWithIds(reference.getSubEntities()));
			attributes.put("equivalent", this.getListWithIds(reference.getSortedEquivalents()));
			attributes.put("minCardinality", this.getCardinality(property.getMinCardinality()));
			attributes.put("maxCardinality", this.getCardinality(property.getMaxCardinality()));
			attributes.put("cardinality", this.getCardinality(property.getExactCardinality()));
			this.propertyList.add(object);
			this.propertyAttributeList.add(attributes);
		} else {
			this.logger.info("Domain or range is empty in property: " + property);
		}
	}

	public void visit(VowlIndividual vowlIndividual) {
	}

	public void visit(TypeOfProperty typeOfProperty) {
		if (!typeOfProperty.getDomains().isEmpty() && !typeOfProperty.getRanges().isEmpty()) {
			HashMap object = new HashMap();
			HashMap attributes = new HashMap();
			this.addCommonFields(typeOfProperty, object, attributes);
			attributes.remove("iri");
			attributes.remove("baseIri");
			attributes.put("domain", this.vowlData.getIdForIri(typeOfProperty.getJsonDomain()));
			attributes.put("range", this.vowlData.getIdForIri(typeOfProperty.getJsonRange()));
			this.propertyList.add(object);
			this.propertyAttributeList.add(attributes);
		} else {
			this.logger.info("Domain or range is empty in typeof property: " + typeOfProperty);
		}
	}

	private List<String> getListWithIds(Collection<IRI> iriList) {
		return (List) iriList.stream().map((iri) -> {
			return String.valueOf(this.vowlData.getIdForIri(iri));
		}).collect(Collectors.toList());
	}

	private String getIdForIri(IRI iri) {
		return iri == null ? null : this.vowlData.getIdForIri(iri);
	}

	private String getCardinality(Integer value) {
		return value.intValue() <= 0 ? "" : value.toString();
	}
}